package zheezes.protocol.impl.gbw;

import java.util.Arrays;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.log4j.Logger;

import zheezes.protocol.Frame;
import zheezes.util.ArrayUtil;
import zheezes.util.ByteUtil;


public class GbwFrame extends Frame {
	public static final int MAX_FRAME_LENGTH = (1 << 9);
	
	public static final int DIRECTION_DOWN = 0;
	public static final int DIRECTION_UP = 1;
	
	public static final int PRM_STARTUP = 1;
	public static final int PRM_DRIVEN = 0;
	
	private static Logger logger = Logger.getLogger(GbwFrame.class);
	
	private byte ctrl;
	private GbwConAddr con = new GbwConAddr();
	private GbwMasterAddr master = new GbwMasterAddr((byte) 0x00);
	private byte afn;
	private byte seq = 0x70; // no TpV, first && last, need to confirm
	private SortedSet<DataUnit> data = new TreeSet<DataUnit>();
	
	static {
		logger.debug("loaded");
	}
		
	/**
	 * the constructure function don't need to know the buffer's length
	 * because it was described in the frame 
	 */
	public GbwFrame() {
	}
	
	public GbwFrame(byte[] buf) {
		setBytes(buf, 0, buf.length);
	}
	
	public GbwFrame(byte[] buf, int offset) {
		setBytes(buf, offset, buf.length);
	}
	
	public int length() {
		return sizeOfData() + 8;
	}

	@Override
	public int size() {
		return length() + 8;
	}
	
	@Override
	public int getBytes(byte[] buf, int offset) {
		int idx = offset;
		buf[idx++] = 0x68;
		short length = (short) length();
		length = (short) (((length << 2) & 0xFFFC) | 0x01);
		ByteUtil.stoc(buf, idx, length);
		idx += 2;
		ByteUtil.stoc(buf, idx, length);
		idx += 2;
		buf[idx++] = 0x68;
		buf[idx++] = ctrl;
		byte mb = master.getData();
		if (afn == 0x02) {
			Arrays.fill(buf, idx, idx + 5, (byte)0x00);
			buf[idx + 2] = mb;
			buf[idx + 4] = 0x02;
			idx += 5;
		} else {
			idx += con.getBytes(buf, idx);
			buf[idx++] = (byte) (mb << 1);
		}
		buf[idx++] = afn;
		buf[idx++] = seq;
		idx += getData(buf, idx);
		buf[idx++] = (byte) ArrayUtil.sum(buf, 6, length());
		buf[idx++] = 0x16;

		return idx - offset;
	}
	
	@Override
	public int setBytes(byte[] buf, int offset, int max) {
		int idx = offset;
		idx++; // 0x68
		short length = ByteUtil.ctos(buf, idx);
		length = (short) ((length >> 2) & 0x3FFF);
		int size = length + 8;
		idx += 5; // L, L, 0x68
		ctrl = buf[idx++];
		afn = buf[12];
		if(afn == 0x02) {
			master = new GbwMasterAddr(buf[idx + 2]);
			idx += 5;
		} else {
			con.setBytes(buf, idx);
			idx += 4;
			master = new GbwMasterAddr((byte) ((buf[idx++] >> 1) & 0x7F));
		}
		setAfn(buf[idx++]);
		setSeq(buf[idx++]);
		idx += setData(buf, idx, offset + size - 2);
		assert idx <= max;
		
		return idx - offset;
	}
	
	@Override
	public boolean match(Frame... frames) {
		for (Frame f : frames) {
			if (!(f instanceof GbwFrame) || (seq != ((GbwFrame) f).seq)) {
				return false;
			}
		}
		return true;
	}

	public int sizeOfData() {
		int size = 0;
		for (DataUnit du : data) {
			size += du.sizeof();
		}
		return size;
	}

	public byte[] getData() {
		byte[] buf = new byte[sizeOfData()];
		getData(buf, 0);
		return buf;
	}

	public int getData(byte[] buf, int offset) {
		int idx = offset;
		for (DataUnit du : data) {
			idx += du.getBytes(buf, idx);
		}
		return idx - offset;
	}

	public int setData(byte[] buf) {
		return setData(buf, 0, buf.length);
	}

	public int setData(byte[] buf, int offset, int max) {
		int idx = offset;
		while(idx < max) {
			DataUnit du = DataUnit.getDataUnit(afn, getDirection(), buf, offset, max);
			data.add(du);
			idx += du.sizeof();
		}
		
		return idx - offset;
	}

	public int addDataUnit(DataUnit du) {
		if (du.getAfn() != afn) {
			return -1;
		}
		data.add(du);
		return data.size();
	}
	
	public DataUnit[] getDataUnits() {
		return data.toArray(new DataUnit[0]);
	}
	
	public int getFseq() {
		return seq & 0x0F;
	}
	
	public void setFseq(int fseq) {
		seq = (byte) ((seq & 0xF0) | (fseq & 0x0F));
	}
	
	public void setAfn(byte afn) {
		this.afn = afn;
	}
	
	public int getDirection() {
		return (ctrl >> 7) & 0x01;
	}
	
	public void setDirection(int dir) {
		if(dir == 0) {
			this.ctrl = (byte) (ctrl & 0x7F);
		} else {
			this.ctrl = (byte) (ctrl | 0x80);
		}
	}
	
	public int getPrm() {
		return (ctrl >> 6) & 0x01;
	}
	
	public void setPrm(int prm) {
		if(prm == 0) {
			this.ctrl = (byte) (ctrl & 0xBF);
		} else {
			this.ctrl = (byte) (ctrl | 0x40);
		}
	}

	/* standard set/get */
	public GbwConAddr getConcentrator() {
		return con;
	}

	public void setConcentrator(GbwConAddr concentrator) {
		this.con = concentrator;
	}

	public GbwMasterAddr getMaster() {
		return master;
	}

	public void setMaster(GbwMasterAddr master) {
		this.master = master;
	}

	public byte getCtrl() {
		return ctrl;
	}

	public void setCtrl(byte ctrl) {
		this.ctrl = ctrl;
	}

	public byte getAfn() {
		return afn;
	}

	public byte getSeq() {
		return seq;
	}

	public void setSeq(byte seq) {
		this.seq = seq;
	}
}
